package cn.highset.redis.service;

import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 注意：list 操作 每条命令 操作的元素不宜过多，即使是管道符操作，也容易造成阻塞
 *
 * @author gg
 * @version RedisListService.java, v 0.1 2019-04-09 13:14 gg
 */
public class RedisListService extends RedisBaseService {

    private RedisTemplate<String, Object> redisTemplate;

    public RedisListService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 对应redis lRange 命令
     * 返回列表 key 中指定区间内的元素，区间以偏移量 start 和 stop 指定。
     * 下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
     * 你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
     * lRange 命令为 闭区间函数，lRange 0 10 会取出 11 个元素
     * 超出范围的下标值不会引起错误。
     * 如果 start 下标比列表的最大下标 end ( lLen list 减去 1 )还要大，那么 lRange 返回一个空列表。
     * 如果 stop 下标比 end 下标还要大，Redis将 stop 的值设置为 end 。
     *
     * @param key   must not be {@literal null}.
     * @param start 开始
     * @param stop  结束
     * @return List<Object>
     * @see <a href="http://redis.io/commands/lrange">Redis Documentation: LRANGE</a>
     */
    public List<Object> lRange(final String key, long start, long stop) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.range(key, start, stop);
    }

    /**
     * 对应命令 lTrim
     * 对一个列表进行修剪(trim)，就是说，让列表只保留指定区间内的元素，不在指定区间之内的元素都将被删除。
     * 下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
     * 你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
     * 当 key 不是列表类型时，返回一个错误
     *
     * @param key   must not be {@literal null}.
     * @param start 开始
     * @param stop  结束
     * @see <a href="http://redis.io/commands/ltrim">Redis Documentation: LTRIM</a>
     */
    public void lTrim(final String key, long start, long stop) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        listOperations.trim(key, start, stop);
    }

    /**
     * 对应 lLen 命令
     * 如果 key 不存在，则 key 被解释为一个空列表，返回 0 .
     * 如果 key 不是列表类型，返回一个错误。
     *
     * @param key must not be {@literal null}.
     * @return Long 返回列表 key 的长度。
     * @see <a href="http://redis.io/commands/llen">Redis Documentation: LLEN</a>
     */
    public Long lLen(final String key) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.size(key);
    }


    /**
     * 对应redis lPush
     * 将一个值 value 插入到列表 key 的表头
     * 如果 key 不存在，一个空列表会被创建并执行 LPUSH 操作。
     * 当 key 存在但不是列表类型时，返回一个错误
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return 执行 lPush 命令后，列表的长度
     * @see <a href="http://redis.io/commands/lpush">Redis Documentation: LPUSH</a>
     */
    public Long lPush(final String key, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPush(key, value);
    }

    /**
     * 对应redis lPush
     * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表头： 比如说，对空列表 myList 执行命令 lPush myList a b c ，
     * 列表的值将是 c b a ，这等同于原子性地执行 lPush myList a 、 lPush myList b 和 lPush myList c 三个命令。
     * 在Redis 2.4版本以前的 lPush 命令，都只接受单个 value 值
     * 当 key 存在但不是列表类型时，返回一个错误
     *
     * @param key    must not be {@literal null}.
     * @param values 可变数组
     * @return 执行 lPush 命令后，列表的长度
     * @see <a href="http://redis.io/commands/lpush">Redis Documentation: LPUSH</a>
     */
    public Long leftPushAll(final String key, Object... values) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPushAll(key, values);
    }

    /**
     * 对应redis lPush
     * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表头： 比如说，对空列表 myList 执行命令 lPush myList a b c ，
     * 列表的值将是 c b a ，这等同于原子性地执行 lPush myList a 、 lPush myList b 和 lPush myList c 三个命令。
     * 在Redis 2.4版本以前的 lPush 命令，都只接受单个 value 值
     * 当 key 存在但不是列表类型时，返回一个错误
     *
     * @param key    must not be {@literal null}.
     * @param values must not be {@literal null}.
     * @return 执行 lPush 命令后，列表的长度
     * @see <a href="http://redis.io/commands/lpush">Redis Documentation: LPUSH</a>
     * @since 1.5
     */
    public Long leftPushAll(final String key, Collection<Object> values) {
        checkKey(key);
        if (CollectionUtils.isEmpty(values)) {
            throw new IllegalArgumentException("列表不能为空");
        }
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPushAll(key, values);
    }

    /**
     * 对应命令 lPushX
     * 将值 value 插入到列表 key 的表头，当且仅当 key 存在并且是一个列表。
     * 和 lPush key value [value …] 命令相反，当 key 不存在时， lPushX 命令什么也不做。
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return lPushX 命令执行之后，表的长度
     * @see <a href="http://redis.io/commands/lpushx">Redis Documentation: LPUSHX</a>
     */
    public Long lPushX(final String key, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPushIfPresent(key, value);
    }

    /**
     * 对应命令 lInsert 此处默认为 Before
     * 将值 value 插入到列表 key 当中，位于值 pivot 之前。
     * 当 pivot 不存在于列表 key 时，不执行任何操作。
     * 当 key 不存在时， key 被视为空列表，不执行任何操作。
     * 如果 key 不是列表类型，返回一个错误。
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return 如果命令执行成功，返回插入操作完成之后，列表的长度。 如果没有找到 pivot ，返回 -1 。 如果 key 不存在或为空列表，返回 0
     * @see <a href="http://redis.io/commands/lpush">Redis Documentation: LPUSH</a>
     */
    public Long leftPush(final String key, Object pivot, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPush(key, pivot, value);
    }

    /**
     * 对应命令 rPush
     * 将一个值 value 插入到列表 key 的表尾(最右边)。
     * 如果 key 不存在，一个空列表会被创建并执行 RPUSH 操作。
     * 当 key 存在但不是列表类型时，返回一个错误。
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return 执行 rPush 操作后，表的长度
     * @see <a href="http://redis.io/commands/rpush">Redis Documentation: RPUSH</a>
     */
    public Long rPush(final String key, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPush(key, value);
    }

    /**
     * 对应命令 rPush
     * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
     * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表尾：比如对一个空列表 myList 执行 rPush myList a b c ，
     * 得出的结果列表为 a b c ，等同于执行命令 rPush myList a 、 rPush myList b 、 rPush myList c
     * 在 Redis 2.4 版本以前的 RPUSH 命令，都只接受单个 value 值
     *
     * @param key    must not be {@literal null}.
     * @param values Object...
     * @return 执行 rPush 操作后，表的长度
     * @see <a href="http://redis.io/commands/rpush">Redis Documentation: RPUSH</a>
     */
    public Long rightPushAll(final String key, Object... values) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPushAll(key, values);
    }

    /**
     * 对应命令 rPush
     * 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
     * 如果有多个 value 值，那么各个 value 值按从左到右的顺序依次插入到表尾：比如对一个空列表 myList 执行 rPush myList a b c ，
     * 得出的结果列表为 a b c ，等同于执行命令 rPush myList a 、 rPush myList b 、 rPush myList c
     * 在 Redis 2.4 版本以前的 RPUSH 命令，都只接受单个 value 值
     *
     * @param key    must not be {@literal null}.
     * @param values Collection
     * @return 执行 rPush 操作后，表的长度
     * @see <a href="http://redis.io/commands/rpush">Redis Documentation: RPUSH</a>
     * @since 1.5
     */
    public Long rightPushAll(final String key, Collection<Object> values) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPushAll(key, values);
    }

    /**
     * 对应命令 rPushX
     * 将值 value 插入到列表 key 的表尾，当且仅当 key 存在并且是一个列表。
     * 和 rPush key value [value …] 命令相反，当 key 不存在时， RPUSHX 命令什么也不做。
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return RPUSHX 命令执行之后，表的长度
     * @see <a href="http://redis.io/commands/rpushx">Redis Documentation: RPUSHX</a>
     */
    public Long rPushX(final String key, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPushIfPresent(key, value);
    }

    /**
     * 对应命令 lInsert
     * 将值 value 插入到列表 key 当中，位于值 pivot之后。
     * 当 pivot 不存在于列表 key 时，不执行任何操作。
     * 当 key 不存在时， key 被视为空列表，不执行任何操作。
     * 如果 key 不是列表类型，返回一个错误。
     *
     * @param key   must not be {@literal null}.
     * @param value Object
     * @return 如果命令执行成功，返回插入操作完成之后，列表的长度。 如果没有找到 pivot ，返回 -1 。 如果 key 不存在或为空列表，返回 0
     * @see <a href="http://redis.io/commands/lpush">Redis Documentation: RPUSH</a>
     */
    public Long rightPush(final String key, Object pivot, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPush(key, pivot, value);
    }

    /**
     * 对应命令 lSet
     * 将列表 key 下标为 index 的元素的值设置为 value
     * 当 index 参数超出范围，或对一个空列表( key 不存在)进行 lSet 时，返回一个错误。
     *
     * @param key   must not be {@literal null}.
     * @param index 下标
     * @param value Object
     * @see <a href="http://redis.io/commands/lset">Redis Documentation: LSET</a>
     */
    public void lSet(final String key, long index, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        listOperations.set(key, index, value);
    }

    /**
     * 对应命令 lRem
     * 根据参数 count 的值，移除列表中与参数 value 相等的元素。
     * count 的值可以是以下几种：
     * count > 0 : 从表头开始向表尾搜索，移除与 value 相等的元素，数量为 count 。
     * count < 0 : 从表尾开始向表头搜索，移除与 value 相等的元素，数量为 count 的绝对值。
     * count = 0 : 移除表中所有与 value 相等的值。
     *
     * @param key   must not be {@literal null}.
     * @param count long
     * @param value Object
     * @return 被移除元素的数量。 因为不存在的 key 被视作空表(empty list)，所以当 key 不存在时， lRem 命令总是返回 0
     * @see <a href="http://redis.io/commands/lrem">Redis Documentation: LREM</a>
     */
    public Long lRem(final String key, long count, Object value) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.remove(key, count, value);
    }

    /**
     * 对应命令 lIndex
     * 下标(index)参数 start 和 stop 都以 0 为底，也就是说，以 0 表示列表的第一个元素，以 1 表示列表的第二个元素，以此类推。
     * 你也可以使用负数下标，以 -1 表示列表的最后一个元素， -2 表示列表的倒数第二个元素，以此类推。
     * 如果 key 不是列表类型，返回一个错误
     *
     * @param key   must not be {@literal null}.
     * @param index 下标
     * @return 列表中下标为 index 的元素。 如果 index 参数的值不在列表的区间范围内(out of range)，返回 null 。
     * @see <a href="http://redis.io/commands/lindex">Redis Documentation: LINDEX</a>
     */
    public Object lIndex(final String key, long index) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.index(key, index);
    }

    /**
     * 对应命令 lPop
     * 移除并返回列表 key 的头元素。
     *
     * @param key must not be {@literal null}.
     * @return 列表的头元素。 当 key 不存在时，返回 null
     * @see <a href="http://redis.io/commands/lpop">Redis Documentation: LPOP</a>
     */
    public Object lPop(final String key) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPop(key);
    }

    /**
     * bLPop 是列表的阻塞式(blocking)弹出原语。
     * 它是 lPop key 命令的阻塞版本，当给定列表内没有任何元素可供弹出的时候，连接将被 BLPOP 命令阻塞，直到等待超时或发现可弹出元素为止。
     * 当给定多个 key 参数时，按参数 key 的先后顺序依次检查各个列表，弹出第一个非空列表的头元素。
     * <p>
     * 非阻塞行为：
     * 当 bLPop 被调用时，如果给定 key 内至少有一个非空列表，那么弹出遇到的第一个非空列表的头元素，
     * 并和被弹出元素所属的列表的名字一起，组成结果返回给调用者
     * <p>
     * 阻塞行为：
     * 如果所有给定 key 都不存在或包含空列表，那么 bLPop 命令将阻塞连接，直到等待超时，
     * 或有另一个客户端对给定 key 的任意一个执行 lPush key value [value …] 或 rPush key value [value …] 命令为止。
     * 超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely) 。
     * <p>
     * 相同的key被多个客户端同时阻塞：
     * 不同的客户端被放进一个队列中，按『先阻塞先服务』(first-bLPop，first-served)的顺序为 key 执行 bLPop 命令。
     * <p>
     * BLPOP 可以用于流水线(pipLine,批量地发送多个命令并读入多个回复)，但把它用在 MULTI / EXEC 块当中没有意义。
     *
     * @param key     must not be {@literal null}.
     * @param timeout 超时时间
     * @param unit    时间单位
     *                NANOSECONDS 纳秒
     *                MICROSECONDS  微秒
     *                MILLISECONDS 毫秒
     *                SECONDS 秒
     *                MINUTES 分钟
     *                HOURS 小时
     *                DAYS  天
     * @return 如果列表为空，返回一个 null 。 否则，返回一个含有两个元素的列表，第一个元素是被弹出元素所属的 key ，第二个元素是被弹出元素的值。
     * @see <a href="http://redis.io/commands/blpop">Redis Documentation: BLPOP</a>
     */
    public Object leftPop(final String key, long timeout, TimeUnit unit) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.leftPop(key, timeout, unit);
    }

    /**
     * 对应 rPop 命令
     * 移除并返回列表 key 的尾元素。
     *
     * @param key must not be {@literal null}.
     * @return 列表的尾元素。 当 key 不存在时，返回 null 。
     * @see <a href="http://redis.io/commands/rpop">Redis Documentation: RPOP</a>
     */
    public Object rPop(final String key) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPop(key);
    }

    /**
     * 对应命令 bRPop
     * 它是 rPop key 命令的阻塞版本，当给定列表内没有任何元素可供弹出的时候，连接将被 bRPop 命令阻塞，直到等待超时或发现可弹出元素为止。
     * 当给定多个 key 参数时，按参数 key 的先后顺序依次检查各个列表，弹出第一个非空列表的尾部元素。
     *
     * @param key     must not be {@literal null}.
     * @param timeout 超时时间
     * @param unit    时间单位
     *                NANOSECONDS 纳秒
     *                MICROSECONDS  微秒
     *                MILLISECONDS 毫秒
     *                SECONDS 秒
     *                MINUTES 分钟
     *                HOURS 小时
     *                DAYS  天
     * @return 假如在指定时间内没有任何元素被弹出，则返回一个 null 和等待时长。 反之，返回一个含有两个元素的列表，
     * 第一个元素是被弹出元素所属的 key ，第二个元素是被弹出元素的值。
     * @see <a href="http://redis.io/commands/brpop">Redis Documentation: BRPOP</a>
     */
    public Object bRPop(final String key, long timeout, TimeUnit unit) {
        checkKey(key);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPop(key, timeout, unit);
    }

    /**
     * 对应命令 rPopLPush
     * 命令 rPopLPush 在一个原子时间内，执行以下两个动作：
     * 将列表 source 中的最后一个元素(尾元素)弹出，并返回给客户端。
     * 将 source 弹出的元素插入到列表 destination ，作为 destination 列表的的头元素。
     * 如果 source 不存在，值 null 被返回，并且不执行其他动作。
     * 如果 source 和 destination 相同，则列表中的表尾元素被移动到表头，并返回该元素，可以把这种特殊情况视作列表的旋转(rotation)操作。
     *
     * @param source      must not be {@literal null}.
     * @param destination must not be {@literal null}.
     * @return 被弹出的元素
     * @see <a href="http://redis.io/commands/rpoplpush">Redis Documentation: RPOPLPUSH</a>
     */
    public Object rPopLPush(final String source, final String destination) {
        checkKey(source);
        checkDestKey(destination);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPopAndLeftPush(source, destination);
    }

    /**
     * 对应命令 bRPopLPush
     * <p>
     * bRPopLPush 是 rPopLPush source destination 的阻塞版本，当给定列表 source 不为空时，
     * bRPopLPush 的表现和 rPopLPush source destination 一样。
     * 当列表 source 为空时， bRPopLPush 命令将阻塞连接，直到等待超时，
     * 或有另一个客户端对 source 执行 lPush key value [value …] 或 rPush key value [value …] 命令为止。
     * <p>
     * 超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely) 。
     *
     * @param source      must not be {@literal null}.
     * @param destination must not be {@literal null}.
     * @param timeout     超时时间
     * @param unit        时间单位
     *                    NANOSECONDS 纳秒
     *                    MICROSECONDS  微秒
     *                    MILLISECONDS 毫秒
     *                    SECONDS 秒
     *                    MINUTES 分钟
     *                    HOURS 小时
     *                    DAYS  天
     * @return 假如在指定时间内没有任何元素被弹出，则返回一个 null 和等待时长。
     * 反之，返回一个含有两个元素的列表，第一个元素是被弹出元素的值，第二个元素是等待时长
     * @see <a href="http://redis.io/commands/brpoplpush">Redis Documentation: BRPOPLPUSH</a>
     */
    public Object bRPopLPush(final String source,
                             final String destination,
                             long timeout, TimeUnit unit) {
        checkKey(source);
        checkDestKey(destination);
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();
        return listOperations.rightPopAndLeftPush(source, destination, timeout, unit);
    }

}